From 36cfd6b9fa8d729172f90e8fdd507349905d4f11 Mon Sep 17 00:00:00 2001 From: tv Date: Fri, 13 Nov 2026 14:01:19 +0000 Subject: populate: follow branches and tags of git sources A ref that consists of 4 to 40 hex digits is a commit, which populate fetches only if the work tree lacks it, unless fetchAlways is set. Any other ref is a branch or tag, which populate fetches on every run, resetting the work tree to the commit fetched. Resetting to the name itself resolves to a local branch, which fetching doesn't move. --- CHANGELOG.md | 6 ++++++ README.md | 6 +++++- pkgs/populate/default.nix | 41 ++++++++++++++++++++++++----------------- tests/scenarios.nix | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d120e39..78721ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changes +## Unreleased + +- `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. + ## 1.31.0 - New source types `flake` and `flake-input` deliver a flake, or one input diff --git a/README.md b/README.md index d88f4e1..2784a9d 100644 --- a/README.md +++ b/README.md @@ -560,7 +560,11 @@ Supported attributes: URL of the Git repository that should be fetched. * `ref` - - Branch / tag / commit that should be fetched. + Branch / tag / commit that should be fetched. A branch or tag gets fetched + on every run, so the work tree follows it. A commit, given by its hash or a + prefix thereof of at least four hex digits, only gets fetched if the work + tree lacks it, unless `fetchAlways` is set. So a branch or tag whose name + consists of hex digits only counts as a commit. * `clean.exclude` - List of patterns that should be excluded from Git cleaning. diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix index a5daee8..1787b65 100644 --- a/pkgs/populate/default.nix +++ b/pkgs/populate/default.nix @@ -1209,25 +1209,32 @@ let git remote set-url origin ${quote source.url} fi - # TODO resolve git_ref to commit hash - hash=${quote source.ref} - - if ! test "$(git log --format=%H -1)" = "$hash"; then - ${if source.fetchAlways then /* sh */ '' - ${if source.shallow then /* sh */ '' - git fetch --depth=1 origin "$hash" - '' else /* sh */ '' - git fetch origin - ''} + # A commit, given by its hash or a prefix thereof, gets fetched only if + # the work tree lacks it, unless fetchAlways is set. A branch or tag + # gets fetched on every run, so the work tree follows it: resetting to + # its name would reset to a local branch, which fetching doesn't move. + ref=${quote source.ref} + fetch() { + ${if source.shallow then /* sh */ '' + git fetch --depth=1 origin "$1" '' else /* sh */ '' - if ! git log -1 "$hash" >/dev/null 2>&1; then - ${if source.shallow then /* sh */ '' - git fetch --depth=1 origin "$hash" - '' else /* sh */ '' - git fetch origin - ''} - fi + git fetch origin "$1" ''} + } + if printf %s "$ref" | grep -Eqx '[0-9a-f]{4,40}'; then + hash=$ref + if ! test "$(git log --format=%H -1)" = "$hash"; then + ${if source.fetchAlways then /* sh */ '' + ${if source.shallow then ''fetch "$hash"'' else "git fetch origin"} + '' else /* sh */ '' + if ! git log -1 "$hash" >/dev/null 2>&1; then + ${if source.shallow then ''fetch "$hash"'' else "git fetch origin"} + fi + ''} + fi + else + fetch "$ref" + hash=$(git rev-parse --verify 'FETCH_HEAD^{commit}') fi # Also if HEAD is there already: rewriting a lock file in place, e.g., # changes the work tree, and stages the lock file if Nix created it. diff --git a/tests/scenarios.nix b/tests/scenarios.nix index 4e869dc..e618fcd 100644 --- a/tests/scenarios.nix +++ b/tests/scenarios.nix @@ -595,6 +595,40 @@ assert !(krops.isLocalTarget (targets.remote "/")); } ]; })) targetKinds + # Branches and tags follow the server: a branch that moves between two + # deploys, and a tag, with full and shallow clones. + ++ concatMap (targetKind: map (shallow: scenario { + name = "git-${targetKind}-shallow${if shallow then "1" else "0"}-refs"; + inherit targetKind; + prepareDeployer = '' + git -C /fixtures/git/gitsrc.git update-ref refs/heads/moving ${gitFirst} + git -C /fixtures/git/gitsrc.git update-ref refs/tags/first ${gitFirst} + ''; + phases = [ + { + sources.x.git = { url = gitUrl; ref = "moving"; inherit shallow; }; + check = '' + expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitFirst} + ''; + mutateDeployer = '' + git -C /fixtures/git/gitsrc.git update-ref refs/heads/moving ${gitHead} + ''; + } + { + sources.x.git = { url = gitUrl; ref = "moving"; inherit shallow; }; + check = '' + expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitHead} + ''; + } + { + sources.x.git = { url = gitUrl; ref = "first"; inherit shallow; }; + check = '' + expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitFirst} + ''; + } + ]; + }) [ false true ]) targetKinds + # A git source stops drawing inputs: its lock file, which Nix created and # staged, goes, so the work tree is as the commit has it. ++ map (targetKind: scenario { -- cgit v1.3.1