diff options
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | pkgs/populate/default.nix | 140 | ||||
| -rwxr-xr-x | tests/fixtures.sh | 17 | ||||
| -rw-r--r-- | tests/scenarios.nix | 60 |
5 files changed, 171 insertions, 55 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 767652b..3875460 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ - Git can't take a `git` source's `url` or `ref`, or a `pass` source's `name`, for an option. +- `git` sources also refuse work trees whose `.git` directory contains + hooks, or in which a submodule's `.git` isn't a file naming a repository + in `.git/modules`, unless `reclone` is set. - `git` sources with a branch or tag as `ref` follow it: populate fetches the branch or tag on every run, and resets the work tree to what it fetched. @@ -316,8 +316,10 @@ everything else. It doesn't change or remove anything outside: `git` source as well. * A `git` source only uses a work tree whose `.git` directory contains no - symlinks, no `commondir` file and no linked work trees, and only the - configuration that `git clone` and populate write. Otherwise Git's + symlinks, no `commondir` file, no linked work trees, and no hooks, and + only the configuration that `git clone` and populate write, and in which + each `.git` of a submodule is a file naming a repository in + `.git/modules`. Otherwise Git's commands could change what's elsewhere, or run commands, so populating fails, telling what's wrong, unless the source's `reclone` is set. Git runs no hooks. diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix index dd0beab..b724b23 100644 --- a/pkgs/populate/default.nix +++ b/pkgs/populate/default.nix @@ -157,14 +157,16 @@ let fi ''; - # Shell code printing why populate can't use the .git directory of the - # Git work tree at path, or nothing. A .git directory that git clone and - # populate made has no symlinks, no commondir file, which names another - # repository, no linked work trees, and only the configuration they write: - # the repository's format, the remote, branches, and submodules, whose - # repositories have their work tree in the work tree. - gitProblems = path: let - gitDir = "${path}/.git"; + # Shell code defining git_problems PATH, which prints why populate can't + # use the .git directory of the Git work tree at PATH, or nothing. A .git + # directory that git clone and populate made has no symlinks, no commondir + # file, which names another repository, no linked work trees, no hooks, + # and only the configuration they write: the repository's format, the + # remote, branches, and submodules, whose repositories have their work + # tree in the work tree. And each .git below the work tree, which Git + # uses for a submodule there, is a file naming a repository in + # .git/modules. + gitProblems = let # Keys of the configuration git clone writes, as patterns of case. keys = [ "core.repositoryformatversion" "core.filemode" "core.bare" @@ -175,49 +177,81 @@ let "branch.*.remote" "branch.*.merge" "submodule.active" "submodule.*.url" "submodule.*.active" ]; - in /* sh */ ''( - links=$(find ${quote gitDir} -type l | head -n 3 | paste -s -d ' ' -) - if test -n "$links"; then - printf '%s contains symlinks: %s' ${quote gitDir} "$links" - exit - fi - common=$(find ${quote gitDir} -name commondir | head -n 1) - if test -n "$common"; then - printf '%s names another repository' "$common" - exit - fi - if test -e ${quote gitDir}/worktrees; then - printf '%s has linked work trees' ${quote gitDir} - exit - fi - find ${quote gitDir} -name config -type f | while IFS= read -r config; do - command git config --file "$config" --list --name-only | - while IFS= read -r key; do - case $key in - ${concatStringsSep "|" keys}) ;; - core.worktree) - # Submodules' repositories have their work tree in the work tree. - worktree=$(command git config --file "$config" core.worktree) - dir=$(dirname "$config") - case $dir/$worktree in - ${quote gitDir}/modules/*) - resolved=$(cd "$dir" && cd "$worktree" 2>/dev/null && pwd -P) || : - case $resolved in - "$(cd ${quote path} && pwd -P)"/*) continue ;; - esac - ;; - esac - printf '%s sets core.worktree to %s' "$config" "$worktree" - exit 1 - ;; - *) - printf '%s sets %s, which neither git clone nor populate does' "$config" "$key" - exit 1 - ;; - esac - done || exit 1 - done || : - )''; + in /* sh */ '' + git_problems() ( + path=$1 + git_dir=$1/.git + links=$(find "$git_dir" -type l | head -n 3 | paste -s -d ' ' -) + if test -n "$links"; then + printf '%s contains symlinks: %s' "$git_dir" "$links" + exit + fi + common=$(find "$git_dir" -name commondir | head -n 1) + if test -n "$common"; then + printf '%s names another repository' "$common" + exit + fi + if test -e "$git_dir"/worktrees; then + printf '%s has linked work trees' "$git_dir" + exit + fi + if test -d "$git_dir"/hooks; then + hooks=$(find "$git_dir"/hooks -mindepth 1 ! -name '*.sample' | head -n 3 | paste -s -d ' ' -) + if test -n "$hooks"; then + printf '%s contains hooks: %s' "$git_dir" "$hooks" + exit + fi + fi + nested=$( + find "$path" -path "$git_dir" -prune -o -name .git -exec sh -c ' + modules=$1 + shift + for git; do + if ! test -L "$git" && test -f "$git"; then + gitdir=$(sed -n "s/^gitdir: //p" "$git") + resolved=$(cd "''${git%/.git}" && cd "$gitdir" 2>/dev/null && pwd -P) || : + case $resolved in + "$modules"/?*) continue ;; + esac + fi + printf "%s\n" "$git" + done + ' sh "$(cd "$git_dir" && pwd -P)/modules" {} + | head -n 1 + ) + if test -n "$nested"; then + printf '%s is not a file naming a repository in %s/modules' "$nested" "$git_dir" + exit + fi + find "$git_dir" -name config -type f | while IFS= read -r config; do + command git config --file "$config" --list --name-only | + while IFS= read -r key; do + case $key in + ${concatStringsSep "|" keys}) ;; + core.worktree) + # Submodules' repositories have their work tree in the work + # tree. + worktree=$(command git config --file "$config" core.worktree) + dir=$(dirname "$config") + case $dir/$worktree in + "$git_dir"/modules/*) + resolved=$(cd "$dir" && cd "$worktree" 2>/dev/null && pwd -P) || : + case $resolved in + "$(cd "$path" && pwd -P)"/*) continue ;; + esac + ;; + esac + printf '%s sets core.worktree to %s' "$config" "$worktree" + exit 1 + ;; + *) + printf '%s sets %s, which neither git clone nor populate does' "$config" "$key" + exit 1 + ;; + esac + done || exit 1 + done || : + ) + ''; check = { force, target }: let sentinelFile = "${target.path}/.populate"; @@ -1164,6 +1198,7 @@ let git() { command git -c core.hooksPath=/dev/null -c core.fsmonitor=false "$@" } + ${gitProblems} # Remove target path if it doesn't look like a git worktree. # This can happen e.g. when it had a different type earlier. A work # tree that a symlink points to, or whose .git is a symlink or a file @@ -1179,8 +1214,7 @@ let # whose .git directory has nothing like that, and else replaces it if # reclone is set, or fails, telling why. if test -e ${quote target.path}; then - # Not $((, which would be arithmetic. - if problems=$( ${gitProblems target.path} ); then :; else + if problems=$(git_problems ${quote target.path}); then :; else exit 1 fi if test -n "$problems"; then diff --git a/tests/fixtures.sh b/tests/fixtures.sh index ccd55a0..85701a1 100755 --- a/tests/fixtures.sh +++ b/tests/fixtures.sh @@ -307,6 +307,23 @@ mkdir /fixtures/git git clone -q --bare /fixtures/gitsrc /fixtures/git/gitsrc.git git -C /fixtures/git/gitsrc.git config uploadpack.allowReachableSHA1InWant true +# withsub: a Git repository with gitsrc as its submodule sub, served by git +# daemon like gitsrc, which the tests reach as git://deployer/gitsrc.git. +mkdir /fixtures/withsub +cat > /fixtures/withsub/.gitmodules <<'GIT' +[submodule "sub"] + path = sub + url = git://deployer/gitsrc.git +GIT +( + cd /fixtures/withsub + git init -q -b master + git update-index --add --cacheinfo 160000,"$git_head",sub + git add .gitmodules + git commit -q -m withsub +) +git clone -q --bare /fixtures/withsub /fixtures/git/withsub.git + # pass: password stores with x/a, without and with Git. if ! gpg --list-secret-keys spam@krebsco.de >/dev/null 2>&1; then gpg --batch --passphrase '' --quick-gen-key spam@krebsco.de default default never diff --git a/tests/scenarios.nix b/tests/scenarios.nix index 8093e02..5d08e2e 100644 --- a/tests/scenarios.nix +++ b/tests/scenarios.nix @@ -2104,6 +2104,66 @@ assert !(krops.isLocalTarget (targets.remote "/")); } ]; })) + # A Git work tree whose submodule's .git, which Git uses as it finds it, + # isn't the gitfile git clone made, naming the repository in + # .git/modules: a repository of its own, whose configuration names a + # command, and a gitfile naming a repository elsewhere. + (s "git-submodule" (let + elsewhere = "/var/krops-elsewhere"; + url = dirOf gitUrl + "/withsub.git"; + git = reclone: { x.git = { inherit url reclone; ref = "master"; }; }; + untouched = '' + test ! -e ${elsewhere}/ran || fail "a command in a .git directory ran" + expect_eq "HEAD elsewhere" "$(git -C ${elsewhere}/repo rev-parse HEAD)" ${gitFirst} + ''; + in { + isolate = true; + prepare = '' + rm -fR ${elsewhere} + git clone -q ${gitUrl} ${elsewhere}/repo + git -C ${elsewhere}/repo checkout -q ${gitFirst} + ''; + phases = [ + { + sources = git false; + check = untouched + '' + expect_eq "sub HEAD" "$(git -C "$T"/x/sub rev-parse HEAD)" ${gitHead} + ''; + mutate = '' + rm "$T"/x/sub/.git + cp -R "$T"/x/.git/modules/sub "$T"/x/sub/.git + git config --file "$T"/x/sub/.git/config --unset core.worktree + git config --file "$T"/x/sub/.git/config \ + filter.x.smudge "touch ${elsewhere}/ran; cat" + echo '* filter=x' > "$T"/x/sub/.git/info/attributes + git -C "$T"/x/sub update-ref --no-deref HEAD ${gitFirst} + git -C "$T"/x/sub read-tree ${gitFirst} + rm "$T"/x/sub/two + ''; + } + { + sources = git false; + expect.fail = "error: populate doesn't use the Git work tree"; + check = untouched; + mutate = '' + rm -fR "$T"/x/sub + mkdir "$T"/x/sub + echo gitdir: ${elsewhere}/repo/.git > "$T"/x/sub/.git + ''; + } + { + sources = git false; + expect.fail = "error: populate doesn't use the Git work tree"; + check = untouched; + } + { + sources = git true; + check = untouched + '' + expect_eq "sub HEAD" "$(git -C "$T"/x/sub rev-parse HEAD)" ${gitHead} + ''; + } + ]; + })) # A ref that git fetch would take for an option: --upload-pack runs a # command where the repository is local. (s "git-ref-option" (let |
