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