at master 16 kB view raw
1#! /usr/bin/env bash 2 3set -e -o pipefail 4 5url= 6rev= 7expHash= 8hashType=$NIX_HASH_ALGO 9deepClone=$NIX_PREFETCH_GIT_DEEP_CLONE 10leaveDotGit=$NIX_PREFETCH_GIT_LEAVE_DOT_GIT 11fetchSubmodules= 12fetchLFS= 13builder= 14fetchTags= 15branchName=$NIX_PREFETCH_GIT_BRANCH_NAME 16 17# ENV params 18out=${out:-} 19http_proxy=${http_proxy:-} 20 21# NOTE: use of NIX_GIT_SSL_CAINFO is for backwards compatibility; NIX_SSL_CERT_FILE is preferred 22# as of PR#303307 23GIT_SSL_CAINFO=${NIX_GIT_SSL_CAINFO:-$NIX_SSL_CERT_FILE} 24 25# populated by clone_user_rev() 26fullRev= 27humanReadableRev= 28commitDate= 29commitDateStrict8601= 30 31if test -n "$deepClone"; then 32 deepClone=true 33else 34 deepClone= 35fi 36 37if test "$leaveDotGit" != 1; then 38 leaveDotGit= 39else 40 leaveDotGit=true 41fi 42 43usage(){ 44 echo >&2 "syntax: nix-prefetch-git [options] [URL [REVISION [EXPECTED-HASH]]] 45 46Options: 47 --out path Path where the output would be stored. 48 --url url Any url understood by 'git clone'. 49 --rev ref Any sha1 or references (such as refs/heads/master) 50 --hash h Expected hash. 51 --name n Symbolic store path name to use for the result (default: based on URL) 52 --branch-name Branch name to check out into 53 --sparse-checkout Only fetch and checkout part of the repository. 54 --non-cone-mode Use non-cone mode for sparse checkouts. 55 --deepClone Clone the entire repository. 56 --no-deepClone Make a shallow clone of just the required ref. 57 --leave-dotGit Keep the .git directories. 58 --fetch-lfs Fetch git Large File Storage (LFS) files. 59 --fetch-submodules Fetch submodules. 60 --fetch-tags Fetch all tags (useful for git describe). 61 --builder Clone as fetchgit does, but url, rev, and out option are mandatory. 62 --no-add-path Do not actually add the contents of the git repo to the store. 63 --root-dir dir Directory in the repository that will be copied to the output instead of the full repository. 64 --quiet Only print the final json summary. 65" 66 exit 1 67} 68 69# some git commands print to stdout, which would contaminate our JSON output 70clean_git(){ 71 git "$@" >&2 72} 73 74argi=0 75argfun="" 76for arg; do 77 if test -z "$argfun"; then 78 case $arg in 79 --out) argfun=set_out;; 80 --url) argfun=set_url;; 81 --rev) argfun=set_rev;; 82 --hash) argfun=set_hashType;; 83 --name) argfun=set_symbolicName;; 84 --branch-name) argfun=set_branchName;; 85 --deepClone) deepClone=true;; 86 --sparse-checkout) argfun=set_sparseCheckout;; 87 --non-cone-mode) nonConeMode=true;; 88 --quiet) QUIET=true;; 89 --no-deepClone) deepClone=;; 90 --leave-dotGit) leaveDotGit=true;; 91 --fetch-lfs) fetchLFS=true;; 92 --fetch-submodules) fetchSubmodules=true;; 93 --fetch-tags) fetchTags=true;; 94 --builder) builder=true;; 95 --no-add-path) noAddPath=true;; 96 --root-dir) argfun=set_rootDir;; 97 -h|--help) usage; exit;; 98 *) 99 : $((++argi)) 100 case $argi in 101 1) url=$arg;; 102 2) rev=$arg;; 103 3) expHash=$arg;; 104 *) exit 1;; 105 esac 106 ;; 107 esac 108 else 109 case $argfun in 110 set_*) 111 var=${argfun#set_} 112 eval "$var=$(printf %q "$arg")" 113 ;; 114 esac 115 argfun="" 116 fi 117done 118 119if test -z "$url"; then 120 usage 121fi 122 123 124init_remote(){ 125 local url=$1 126 clean_git init --initial-branch=master 127 clean_git remote add origin "$url" 128 if [ -n "$sparseCheckout" ]; then 129 git config remote.origin.partialclonefilter "blob:none" 130 echo "$sparseCheckout" | git sparse-checkout set --stdin ${nonConeMode:+--no-cone} 131 fi 132 ( [ -n "$http_proxy" ] && clean_git config --global http.proxy "$http_proxy" ) || true 133 local proxy_pairs i 134 read -a proxy_pairs <<< "${FETCHGIT_HTTP_PROXIES:-}" 135 for ((i = 1; i < ${#proxy_pairs[@]}; i += 2)); do 136 clean_git config --global "http.${proxy_pairs[$i - 1]}.proxy" "${proxy_pairs[$i]}" 137 done 138} 139 140# Return the reference of an hash if it exists on the remote repository. 141ref_from_hash(){ 142 local hash=$1 143 git ls-remote origin | sed -n "\,$hash\t, { s,\(.*\)\t\(.*\),\2,; p; q}" 144} 145 146# Return the hash of a reference if it exists on the remote repository. 147hash_from_ref(){ 148 local ref=$1 149 git ls-remote origin | sed -n "\,\t$ref, { s,\(.*\)\t\(.*\),\1,; p; q}" 150} 151 152# Returns a name based on the url and reference 153# 154# This function needs to be in sync with nix's fetchgit implementation 155# of urlToName() to re-use the same nix store paths. 156url_to_name(){ 157 local url=$1 158 local ref=$2 159 local base 160 base=$(basename "$url" .git | cut -d: -f2) 161 162 if test -n "$rootDir"; then 163 # Sanitize by removing leading dots and replacing all invalid character sequences with dashes. 164 # See sanitizeDerivationName in ../../../lib/strings.nix for reference. 165 echo "$base-$(sed -E 's/^\.+//;s/[^[:alnum:]+._?=-]+/-/g' <<< $rootDir)" 166 else 167 if [[ $ref =~ ^[a-z0-9]+$ ]]; then 168 echo "$base-${ref:0:7}" 169 else 170 echo "$base" 171 fi 172 fi 173} 174 175# Fetch and checkout the right sha1 176checkout_hash(){ 177 local hash="$1" 178 local ref="$2" 179 180 if test -z "$hash"; then 181 hash=$(hash_from_ref "$ref") 182 fi 183 184 [[ -z "$deepClone" ]] && \ 185 clean_git fetch ${builder:+--progress} --depth=1 origin "$hash" || \ 186 clean_git fetch -t ${builder:+--progress} origin || return 1 187 188 local object_type=$(git cat-file -t "$hash") 189 if [[ "$object_type" == "commit" || "$object_type" == "tag" ]]; then 190 clean_git checkout -b "$branchName" "$hash" || return 1 191 elif [[ "$object_type" == "tree" ]]; then 192 clean_git config user.email "nix-prefetch-git@localhost" 193 clean_git config user.name "nix-prefetch-git" 194 local commit_id=$(git commit-tree "$hash" -m "Commit created from tree hash $hash") 195 clean_git checkout -b "$branchName" "$commit_id" || return 1 196 else 197 echo "Unrecognized git object type: $object_type" 198 return 1 199 fi 200} 201 202# Fetch only a branch/tag and checkout it. 203checkout_ref(){ 204 local hash="$1" 205 local ref="$2" 206 207 if [[ -n "$deepClone" ]]; then 208 # The caller explicitly asked for a deep clone. Deep clones 209 # allow "git describe" and similar tools to work. See 210 # https://marc.info/?l=nix-dev&m=139641582514772 211 # for a discussion. 212 return 1 213 fi 214 215 if test -z "$ref"; then 216 ref=$(ref_from_hash "$hash") 217 fi 218 219 if test -n "$ref"; then 220 # --depth option is ignored on http repository. 221 clean_git fetch ${builder:+--progress} --depth 1 origin +"$ref" || return 1 222 clean_git checkout -b "$branchName" FETCH_HEAD || return 1 223 else 224 return 1 225 fi 226} 227 228# Update submodules 229init_submodules(){ 230 # shallow with leaveDotGit will change hashes 231 [[ -z "$deepClone" ]] && [[ -z "$leaveDotGit" ]] && \ 232 clean_git submodule update --init --recursive --checkout -j ${NIX_BUILD_CORES:-1} --progress --depth 1 || \ 233 clean_git submodule update --init --recursive --checkout -j ${NIX_BUILD_CORES:-1} --progress 234} 235 236clone(){ 237 local top=$PWD 238 local dir="$1" 239 local url="$2" 240 local hash="$3" 241 local ref="$4" 242 243 cd "$dir" 244 245 # Initialize the repository. 246 init_remote "$url" 247 248 # Download data from the repository. 249 checkout_ref "$hash" "$ref" || 250 checkout_hash "$hash" "$ref" || ( 251 echo 1>&2 "Unable to checkout $hash$ref from $url." 252 exit 1 253 ) 254 255 # Fetch all tags if requested 256 if test -n "$fetchTags"; then 257 echo "fetching all tags..." >&2 258 clean_git fetch origin 'refs/tags/*:refs/tags/*' || echo "warning: failed to fetch some tags" >&2 259 fi 260 261 # Checkout linked sources. 262 if test -n "$fetchSubmodules"; then 263 init_submodules 264 fi 265 266 if [ -z "$builder" ] && [ -f .topdeps ]; then 267 if tg help &>/dev/null; then 268 echo "populating TopGit branches..." 269 tg remote --populate origin 270 else 271 echo "WARNING: would populate TopGit branches but TopGit is not available" >&2 272 echo "WARNING: install TopGit to fix the problem" >&2 273 fi 274 fi 275 276 cd "$top" 277} 278 279# Remove all remote branches, remove tags not reachable from HEAD, do a full 280# repack and then garbage collect unreferenced objects. 281make_deterministic_repo(){ 282 local repo="$1" 283 284 # run in sub-shell to not touch current working directory 285 ( 286 cd "$repo" 287 # Remove files that contain timestamps or otherwise have non-deterministic 288 # properties. 289 if [ -f .git ]; then 290 local dotgit_content=$(<.git) 291 local dotgit_dir="${dotgit_content#gitdir: }" 292 else 293 local dotgit_dir=".git" 294 fi 295 pushd "$dotgit_dir" >/dev/null 296 rm -rf logs/ hooks/ index FETCH_HEAD ORIG_HEAD refs/remotes/origin/HEAD config 297 popd >/dev/null 298 # Remove all remote branches. 299 git branch -r | while read -r branch; do 300 clean_git branch -rD "$branch" 301 done 302 303 # Remove tags not reachable from HEAD. If we're exactly on a tag, don't 304 # delete it. 305 maybe_tag=$(git tag --points-at HEAD) 306 git tag --contains HEAD | while read -r tag; do 307 if [ "$tag" != "$maybe_tag" ]; then 308 clean_git tag -d "$tag" 309 fi 310 done 311 312 # Do a full repack. Must run single-threaded, or else we lose determinism. 313 clean_git config pack.threads 1 314 clean_git repack -A -d -f 315 rm -f "$dotgit_dir/config" 316 317 # Garbage collect unreferenced objects. 318 # Note: --keep-largest-pack prevents non-deterministic ordering of packs 319 # listed in .git/objects/info/packs by only using a single pack 320 clean_git gc --prune=all --keep-largest-pack 321 ) 322} 323 324 325clone_user_rev() { 326 local dir="$1" 327 local url="$2" 328 local rev="${3:-HEAD}" 329 330 if [ -n "$fetchLFS" ]; then 331 clean_git lfs install 332 fi 333 334 # Perform the checkout. 335 case "$rev" in 336 HEAD|refs/*) 337 clone "$dir" "$url" "" "$rev" 1>&2;; 338 *) 339 if test -z "$(echo "$rev" | tr -d 0123456789abcdef)"; then 340 clone "$dir" "$url" "$rev" "" 1>&2 341 else 342 # if revision is not hexadecimal it might be a tag 343 clone "$dir" "$url" "" "refs/tags/$rev" 1>&2 344 fi;; 345 esac 346 347 pushd "$dir" >/dev/null 348 fullRev=$( (git rev-parse "$rev" 2>/dev/null || git rev-parse "refs/heads/$branchName") | tail -n1) 349 humanReadableRev=$(git describe "$fullRev" 2> /dev/null || git describe --tags "$fullRev" 2> /dev/null || echo -- none --) 350 commitDate=$(git show -1 --no-patch --pretty=%ci "$fullRev") 351 commitDateStrict8601=$(git show -1 --no-patch --pretty=%cI "$fullRev") 352 popd >/dev/null 353 354 # Allow doing additional processing before .git removal 355 eval "$NIX_PREFETCH_GIT_CHECKOUT_HOOK" 356 if test -z "$leaveDotGit"; then 357 echo "removing \`.git'..." >&2 358 find "$dir" -name .git -print0 | xargs -0 rm -rf 359 else 360 find "$dir" -name .git | while read -r gitdir; do 361 make_deterministic_repo "$(readlink -f "$(dirname "$gitdir")")" 362 done 363 fi 364} 365 366clone_user_rev_to_tmpfile(){ 367 local url="$1" 368 local rev="${2:-HEAD}" 369 370 # nix>=2.20 rejects adding symlinked paths to the store, so use realpath 371 # to resolve to a physical path. https://github.com/NixOS/nix/issues/11941 372 tmpPath="$(realpath "$(mktemp -d --tmpdir git-checkout-tmp-XXXXXXXX)")" 373 exit_handlers+=(remove_tmpPath) 374 375 tmpOut="$tmpPath/out/$storePathName" 376 tmpClone="$tmpPath/clone" 377 mkdir -p "$tmpPath/out" "$tmpClone" 378 379 # Perform the checkout. 380 clone_user_rev "$tmpClone" "$url" "$rev" 381} 382 383exit_handlers=() 384 385run_exit_handlers() { 386 exit_status=$? 387 for handler in "${exit_handlers[@]}"; do 388 eval "$handler $exit_status" 389 done 390} 391 392trap run_exit_handlers EXIT 393 394quiet_exit_handler() { 395 exec 2>&3 3>&- 396 if [ $1 -ne 0 ]; then 397 cat "$errfile" >&2 398 fi 399 rm -f "$errfile" 400} 401 402quiet_mode() { 403 errfile="$(mktemp "${TMPDIR:-/tmp}/git-checkout-err-XXXXXXXX")" 404 exit_handlers+=(quiet_exit_handler) 405 exec 3>&2 2>"$errfile" 406} 407 408json_escape() { 409 local s="$1" 410 s="${s//\\/\\\\}" # \ 411 s="${s//\"/\\\"}" # " 412 s="${s//^H/\\\b}" # \b (backspace) 413 s="${s//^L/\\\f}" # \f (form feed) 414 s="${s// 415/\\\n}" # \n (newline) 416 s="${s//^M/\\\r}" # \r (carriage return) 417 s="${s// /\\t}" # \t (tab) 418 echo "$s" 419} 420 421print_results() { 422 hash="$1" 423 if ! test -n "$QUIET"; then 424 echo "" >&2 425 echo "git revision is $fullRev" >&2 426 if test -n "$finalPath"; then 427 echo "path is $finalPath" >&2 428 fi 429 echo "git human-readable version is $humanReadableRev" >&2 430 echo "Commit date is $commitDate" >&2 431 if test -n "$hash"; then 432 echo "hash is $hash" >&2 433 fi 434 fi 435 if test -n "$hash"; then 436 cat <<EOF 437{ 438 "url": "$(json_escape "$url")", 439 "rev": "$(json_escape "$fullRev")", 440 "date": "$(json_escape "$commitDateStrict8601")", 441 "path": "$(json_escape "$finalPath")", 442 "$(json_escape "$hashType")": "$(json_escape "$hash")", 443 "hash": "$(nix-hash --to-sri --type $hashType $hash)", 444 "fetchLFS": $([[ -n "$fetchLFS" ]] && echo true || echo false), 445 "fetchSubmodules": $([[ -n "$fetchSubmodules" ]] && echo true || echo false), 446 "deepClone": $([[ -n "$deepClone" ]] && echo true || echo false), 447 "fetchTags": $([[ -n "$fetchTags" ]] && echo true || echo false), 448 "leaveDotGit": $([[ -n "$leaveDotGit" ]] && echo true || echo false), 449 "rootDir": "$(json_escape "$rootDir")" 450} 451EOF 452 fi 453} 454 455remove_tmpPath() { 456 rm -rf "$tmpPath" 457} 458 459remove_tmpHomePath() { 460 chmod -R u+w "$tmpHomePath" 461 rm -rf "$tmpHomePath" 462} 463 464if test -n "$QUIET"; then 465 quiet_mode 466fi 467 468if test -z "$branchName"; then 469 branchName=fetchgit 470fi 471 472if [ -v symbolicName ]; then 473 storePathName="$symbolicName" 474else 475 storePathName="$(url_to_name "$url" "$rev")" 476fi 477 478tmpHomePath="$(mktemp -d "${TMPDIR:-/tmp}/nix-prefetch-git-tmp-home-XXXXXXXXXX")" 479exit_handlers+=(remove_tmpHomePath) 480ln -s "${NETRC:-$HOME/.netrc}" "$tmpHomePath/.netrc" 481HOME="$tmpHomePath" 482unset XDG_CONFIG_HOME 483export GIT_CONFIG_NOSYSTEM=1 484 485if test -n "$builder"; then 486 test -n "$out" -a -n "$url" -a -n "$rev" || usage 487 if test -n "$rootDir"; then 488 clone_user_rev_to_tmpfile "$url" "$rev" 489 mv "$tmpClone/$rootDir" "$out" 490 else 491 mkdir -p "$out" 492 clone_user_rev "$out" "$url" "$rev" 493 fi 494else 495 if test -z "$hashType"; then 496 hashType=sha256 497 fi 498 499 # If the hash was given, a file with that hash may already be in the 500 # store. 501 if test -n "$expHash"; then 502 finalPath=$(nix-store --print-fixed-path --recursive "$hashType" "$expHash" "$storePathName") 503 if ! nix-store --check-validity "$finalPath" 2> /dev/null; then 504 finalPath= 505 fi 506 hash=$expHash 507 fi 508 509 # If we don't know the hash or a path with that hash doesn't exist, 510 # download the file and add it to the store. 511 if test -z "$finalPath"; then 512 clone_user_rev_to_tmpfile "$url" "$rev" 513 514 if test -z "$rootDir"; then 515 mv "$tmpClone" "$tmpOut" 516 else 517 mv "$tmpClone/$rootDir" "$tmpOut" 518 fi 519 520 # Compute the hash. 521 hash=$(nix-hash --type $hashType --base32 "$tmpOut") 522 523 # Add the downloaded file to the Nix store. 524 525 if test -z "$noAddPath"; then 526 finalPath=$(nix-store --add-fixed --recursive "$hashType" "$tmpOut") \ 527 || { printf "maybe try again with \`nix-prefetch-git --no-add-path <...>' ?\n" >&2; exit 1; } 528 else 529 printf "the path for \`%s' has NOT been added to the store\n" "$url" >&2 530 finalPath=$(nix-store --print-fixed-path --recursive "$hashType" "$hash" "$storePathName") 531 fi 532 533 if test -n "$expHash" -a "$expHash" != "$hash"; then 534 echo "hash mismatch for URL \`$url'. Got \`$hash'; expected \`$expHash'." >&2 535 exit 1 536 fi 537 fi 538 539 print_results "$hash" 540 541 if test -n "$PRINT_PATH"; then 542 echo "$finalPath" 543 fi 544fi