Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at 20.03 439 lines 12 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= 12builder= 13branchName=$NIX_PREFETCH_GIT_BRANCH_NAME 14 15# ENV params 16out=${out:-} 17http_proxy=${http_proxy:-} 18 19# populated by clone_user_rev() 20fullRev= 21humanReadableRev= 22commitDate= 23commitDateStrict8601= 24 25if test -n "$deepClone"; then 26 deepClone=true 27else 28 deepClone=false 29fi 30 31if test "$leaveDotGit" != 1; then 32 leaveDotGit= 33else 34 leaveDotGit=true 35fi 36 37usage(){ 38 echo >&2 "syntax: nix-prefetch-git [options] [URL [REVISION [EXPECTED-HASH]]] 39 40Options: 41 --out path Path where the output would be stored. 42 --url url Any url understood by 'git clone'. 43 --rev ref Any sha1 or references (such as refs/heads/master) 44 --hash h Expected hash. 45 --branch-name Branch name to check out into 46 --deepClone Clone the entire repository. 47 --no-deepClone Make a shallow clone of just the required ref. 48 --leave-dotGit Keep the .git directories. 49 --fetch-submodules Fetch submodules. 50 --builder Clone as fetchgit does, but url, rev, and out option are mandatory. 51 --quiet Only print the final json summary. 52" 53 exit 1 54} 55 56argi=0 57argfun="" 58for arg; do 59 if test -z "$argfun"; then 60 case $arg in 61 --out) argfun=set_out;; 62 --url) argfun=set_url;; 63 --rev) argfun=set_rev;; 64 --hash) argfun=set_hashType;; 65 --branch-name) argfun=set_branchName;; 66 --deepClone) deepClone=true;; 67 --quiet) QUIET=true;; 68 --no-deepClone) deepClone=false;; 69 --leave-dotGit) leaveDotGit=true;; 70 --fetch-submodules) fetchSubmodules=true;; 71 --builder) builder=true;; 72 -h|--help) usage; exit;; 73 *) 74 : $((++argi)) 75 case $argi in 76 1) url=$arg;; 77 2) rev=$arg;; 78 3) expHash=$arg;; 79 *) exit 1;; 80 esac 81 ;; 82 esac 83 else 84 case $argfun in 85 set_*) 86 var=${argfun#set_} 87 eval $var=$arg 88 ;; 89 esac 90 argfun="" 91 fi 92done 93 94if test -z "$url"; then 95 usage 96fi 97 98 99init_remote(){ 100 local url=$1 101 git init 102 git remote add origin "$url" 103 ( [ -n "$http_proxy" ] && git config http.proxy "$http_proxy" ) || true 104} 105 106# Return the reference of an hash if it exists on the remote repository. 107ref_from_hash(){ 108 local hash=$1 109 git ls-remote origin | sed -n "\,$hash\t, { s,\(.*\)\t\(.*\),\2,; p; q}" 110} 111 112# Return the hash of a reference if it exists on the remote repository. 113hash_from_ref(){ 114 local ref=$1 115 git ls-remote origin | sed -n "\,\t$ref, { s,\(.*\)\t\(.*\),\1,; p; q}" 116} 117 118# Returns a name based on the url and reference 119# 120# This function needs to be in sync with nix's fetchgit implementation 121# of urlToName() to re-use the same nix store paths. 122url_to_name(){ 123 local url=$1 124 local ref=$2 125 local base 126 base=$(basename "$url" .git | cut -d: -f2) 127 128 if [[ $ref =~ ^[a-z0-9]+$ ]]; then 129 echo "$base-${ref:0:7}" 130 else 131 echo "$base" 132 fi 133} 134 135# Fetch everything and checkout the right sha1 136checkout_hash(){ 137 local hash="$1" 138 local ref="$2" 139 140 if test -z "$hash"; then 141 hash=$(hash_from_ref "$ref") 142 fi 143 144 git fetch -t ${builder:+--progress} origin || return 1 145 git checkout -b "$branchName" "$hash" || return 1 146} 147 148# Fetch only a branch/tag and checkout it. 149checkout_ref(){ 150 local hash="$1" 151 local ref="$2" 152 153 if "$deepClone"; then 154 # The caller explicitly asked for a deep clone. Deep clones 155 # allow "git describe" and similar tools to work. See 156 # https://marc.info/?l=nix-dev&m=139641582514772 157 # for a discussion. 158 return 1 159 fi 160 161 if test -z "$ref"; then 162 ref=$(ref_from_hash "$hash") 163 fi 164 165 if test -n "$ref"; then 166 # --depth option is ignored on http repository. 167 git fetch ${builder:+--progress} --depth 1 origin +"$ref" || return 1 168 git checkout -b "$branchName" FETCH_HEAD || return 1 169 else 170 return 1 171 fi 172} 173 174# Update submodules 175init_submodules(){ 176 # Add urls into .git/config file 177 git submodule init 178 179 # list submodule directories and their hashes 180 git submodule status | 181 while read -r l; do 182 local hash 183 local dir 184 local name 185 local url 186 187 # checkout each submodule 188 hash=$(echo "$l" | awk '{print $1}' | tr -d '-') 189 dir=$(echo "$l" | sed -n 's/^.[0-9a-f]\+ \(.*[^)]*\)\( (.*)\)\?$/\1/p') 190 name=$( 191 git config -f .gitmodules --get-regexp submodule\..*\.path | 192 sed -n "s,^\(.*\)\.path $dir\$,\\1,p") 193 url=$(git config --get "${name}.url") 194 195 clone "$dir" "$url" "$hash" "" 196 done 197} 198 199clone(){ 200 local top=$PWD 201 local dir="$1" 202 local url="$2" 203 local hash="$3" 204 local ref="$4" 205 206 cd "$dir" 207 208 # Initialize the repository. 209 init_remote "$url" 210 211 # Download data from the repository. 212 checkout_ref "$hash" "$ref" || 213 checkout_hash "$hash" "$ref" || ( 214 echo 1>&2 "Unable to checkout $hash$ref from $url." 215 exit 1 216 ) 217 218 # Checkout linked sources. 219 if test -n "$fetchSubmodules"; then 220 init_submodules 221 fi 222 223 if [ -z "$builder" ] && [ -f .topdeps ]; then 224 if tg help &>/dev/null; then 225 echo "populating TopGit branches..." 226 tg remote --populate origin 227 else 228 echo "WARNING: would populate TopGit branches but TopGit is not available" >&2 229 echo "WARNING: install TopGit to fix the problem" >&2 230 fi 231 fi 232 233 cd "$top" 234} 235 236# Remove all remote branches, remove tags not reachable from HEAD, do a full 237# repack and then garbage collect unreferenced objects. 238make_deterministic_repo(){ 239 local repo="$1" 240 241 # run in sub-shell to not touch current working directory 242 ( 243 cd "$repo" 244 # Remove files that contain timestamps or otherwise have non-deterministic 245 # properties. 246 rm -rf .git/logs/ .git/hooks/ .git/index .git/FETCH_HEAD .git/ORIG_HEAD \ 247 .git/refs/remotes/origin/HEAD .git/config 248 249 # Remove all remote branches. 250 git branch -r | while read -r branch; do 251 git branch -rD "$branch" >&2 252 done 253 254 # Remove tags not reachable from HEAD. If we're exactly on a tag, don't 255 # delete it. 256 maybe_tag=$(git tag --points-at HEAD) 257 git tag --contains HEAD | while read -r tag; do 258 if [ "$tag" != "$maybe_tag" ]; then 259 git tag -d "$tag" >&2 260 fi 261 done 262 263 # Do a full repack. Must run single-threaded, or else we lose determinism. 264 git config pack.threads 1 265 git repack -A -d -f 266 rm -f .git/config 267 268 # Garbage collect unreferenced objects. 269 # Note: --keep-largest-pack prevents non-deterministic ordering of packs 270 # listed in .git/objects/info/packs by only using a single pack 271 git gc --prune=all --keep-largest-pack 272 ) 273} 274 275 276clone_user_rev() { 277 local dir="$1" 278 local url="$2" 279 local rev="${3:-HEAD}" 280 281 # Perform the checkout. 282 case "$rev" in 283 HEAD|refs/*) 284 clone "$dir" "$url" "" "$rev" 1>&2;; 285 *) 286 if test -z "$(echo "$rev" | tr -d 0123456789abcdef)"; then 287 clone "$dir" "$url" "$rev" "" 1>&2 288 else 289 # if revision is not hexadecimal it might be a tag 290 clone "$dir" "$url" "" "refs/tags/$rev" 1>&2 291 fi;; 292 esac 293 294 pushd "$dir" >/dev/null 295 fullRev=$( (git rev-parse "$rev" 2>/dev/null || git rev-parse "refs/heads/$branchName") | tail -n1) 296 humanReadableRev=$(git describe "$fullRev" 2> /dev/null || git describe --tags "$fullRev" 2> /dev/null || echo -- none --) 297 commitDate=$(git show -1 --no-patch --pretty=%ci "$fullRev") 298 commitDateStrict8601=$(git show -1 --no-patch --pretty=%cI "$fullRev") 299 popd >/dev/null 300 301 # Allow doing additional processing before .git removal 302 eval "$NIX_PREFETCH_GIT_CHECKOUT_HOOK" 303 if test -z "$leaveDotGit"; then 304 echo "removing \`.git'..." >&2 305 find "$dir" -name .git -print0 | xargs -0 rm -rf 306 else 307 find "$dir" -name .git | while read -r gitdir; do 308 make_deterministic_repo "$(readlink -f "$gitdir/..")" 309 done 310 fi 311} 312 313exit_handlers=() 314 315run_exit_handlers() { 316 exit_status=$? 317 for handler in "${exit_handlers[@]}"; do 318 eval "$handler $exit_status" 319 done 320} 321 322trap run_exit_handlers EXIT 323 324quiet_exit_handler() { 325 exec 2>&3 3>&- 326 if [ $1 -ne 0 ]; then 327 cat "$errfile" >&2 328 fi 329 rm -f "$errfile" 330} 331 332quiet_mode() { 333 errfile="$(mktemp "${TMPDIR:-/tmp}/git-checkout-err-XXXXXXXX")" 334 exit_handlers+=(quiet_exit_handler) 335 exec 3>&2 2>"$errfile" 336} 337 338json_escape() { 339 local s="$1" 340 s="${s//\\/\\\\}" # \ 341 s="${s//\"/\\\"}" # " 342 s="${s//^H/\\\b}" # \b (backspace) 343 s="${s//^L/\\\f}" # \f (form feed) 344 s="${s// 345/\\\n}" # \n (newline) 346 s="${s//^M/\\\r}" # \r (carriage return) 347 s="${s// /\\t}" # \t (tab) 348 echo "$s" 349} 350 351print_results() { 352 hash="$1" 353 if ! test -n "$QUIET"; then 354 echo "" >&2 355 echo "git revision is $fullRev" >&2 356 if test -n "$finalPath"; then 357 echo "path is $finalPath" >&2 358 fi 359 echo "git human-readable version is $humanReadableRev" >&2 360 echo "Commit date is $commitDate" >&2 361 if test -n "$hash"; then 362 echo "hash is $hash" >&2 363 fi 364 fi 365 if test -n "$hash"; then 366 cat <<EOF 367{ 368 "url": "$(json_escape "$url")", 369 "rev": "$(json_escape "$fullRev")", 370 "date": "$(json_escape "$commitDateStrict8601")", 371 "$(json_escape "$hashType")": "$(json_escape "$hash")", 372 "fetchSubmodules": $([[ -n "$fetchSubmodules" ]] && echo true || echo false) 373} 374EOF 375 fi 376} 377 378remove_tmpPath() { 379 rm -rf "$tmpPath" 380} 381 382if test -n "$QUIET"; then 383 quiet_mode 384fi 385 386if test -z "$branchName"; then 387 branchName=fetchgit 388fi 389 390if test -n "$builder"; then 391 test -n "$out" -a -n "$url" -a -n "$rev" || usage 392 mkdir -p "$out" 393 clone_user_rev "$out" "$url" "$rev" 394else 395 if test -z "$hashType"; then 396 hashType=sha256 397 fi 398 399 # If the hash was given, a file with that hash may already be in the 400 # store. 401 if test -n "$expHash"; then 402 finalPath=$(nix-store --print-fixed-path --recursive "$hashType" "$expHash" "$(url_to_name "$url" "$rev")") 403 if ! nix-store --check-validity "$finalPath" 2> /dev/null; then 404 finalPath= 405 fi 406 hash=$expHash 407 fi 408 409 # If we don't know the hash or a path with that hash doesn't exist, 410 # download the file and add it to the store. 411 if test -z "$finalPath"; then 412 413 tmpPath="$(mktemp -d "${TMPDIR:-/tmp}/git-checkout-tmp-XXXXXXXX")" 414 exit_handlers+=(remove_tmpPath) 415 416 tmpFile="$tmpPath/$(url_to_name "$url" "$rev")" 417 mkdir -p "$tmpFile" 418 419 # Perform the checkout. 420 clone_user_rev "$tmpFile" "$url" "$rev" 421 422 # Compute the hash. 423 hash=$(nix-hash --type $hashType --base32 "$tmpFile") 424 425 # Add the downloaded file to the Nix store. 426 finalPath=$(nix-store --add-fixed --recursive "$hashType" "$tmpFile") 427 428 if test -n "$expHash" -a "$expHash" != "$hash"; then 429 echo "hash mismatch for URL \`$url'. Got \`$hash'; expected \`$expHash'." >&2 430 exit 1 431 fi 432 fi 433 434 print_results "$hash" 435 436 if test -n "$PRINT_PATH"; then 437 echo "$finalPath" 438 fi 439fi